/**
 * Copyright (c) 2003 held jointly by the individual authors.            
 *                                                                          
 * This library is free software; you can redistribute it and/or modify it    
 * under the terms of the GNU Lesser General Public License as published      
 * by the Free Software Foundation; either version 2.1 of the License, or 
 * (at your option) any later version.                                            
 *                                                                            
 * This library is distributed in the hope that it will be useful, but 
 * WITHOUT ANY WARRANTY; with out even the implied warranty of 
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 * GNU Lesser General Public License for more details.                                                  
 *                                                                           
 * You should have received a copy of the GNU Lesser General Public License   
 * along with this library;  if not, write to the Free Software Foundation,   
 * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA.              
 *                                                                            
 * > http://www.gnu.org/copyleft/lesser.html                                  
 * > http://www.opensource.org/licenses/lgpl-license.php
 */
package net.mlw.vlh.adapter.hibernate3;

import java.lang.reflect.InvocationTargetException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;

import net.mlw.vlh.DefaultListBackedValueList;
import net.mlw.vlh.ValueList;
import net.mlw.vlh.ValueListInfo;
import net.mlw.vlh.adapter.AbstractValueListAdapter;
import net.mlw.vlh.adapter.hibernate3.util.ScrollableResultsDecorator;
import net.mlw.vlh.adapter.hibernate3.util.StatementBuilder;
import net.mlw.vlh.adapter.util.ObjectValidator;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.HibernateException;
import org.hibernate.Query;
import org.hibernate.ScrollableResults;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.orm.hibernate3.SessionFactoryUtils;

/** 
 *  This adapter wraps the functionality of Hibernate.
 *  Add extra functionality such as paging, focusing
 *  and validating of current result set.
 * <i>
 *   "Hibernate is a powerful, ultra-high performance 
 *   object/relational persistence and query service 
 *   for Java. Hibernate lets you develop persistent 
 *   classes following common Java idiom - including 
 *   association, inheritance, polymorphism, composition 
 *   and the Java collections framework. The Hibernate 
 *   Query Language, designed as a "minimal" 
 *   object-oriented extension to SQL, provides an 
 *   elegant bridge between the object and relational 
 *   worlds. Hibernate also allows you to express queries 
 *   using native SQL or Java-based Criteria and Example 
 *   queries. Hibernate is now the most popular 
 *   object/relational mapping solution for Java."
 * </i> -http://www.hibernate.org/
 * 
 * @author Matthew L. Wilson, Andrej Zachar
 * @version $Revision: 1.2 $ $Date: 2006/03/29 19:47:49 $
 */
public class HibernateAdapter extends AbstractValueListAdapter
{
   /** The Hibernate SessionFactory. */
   private SessionFactory sessionFactory;

   /**
    * <p>
    * If is set, it use special ScrollableResultsDecorator, that enable or
    * disable to add object in final list.
    * </p>
    * <h4>NOTE:</h4>
    * <p>
    * Also, it respects the total count of entries that overlap your paged
    * list.
    * </p>
    */
   private ObjectValidator _validator = null;

   /** Commons logger. */
   private static final Log LOGGER = LogFactory.getLog(HibernateAdapter.class);

   /** If a new Session should be created if no thread-bound found. */
   private boolean allowCreate = true;

   /** The hibernate query. */
   private String hql;

   private String namedQuery;

   /** The max rows in ResulSet to doFocus 
    *  @author Andrej Zachar
    */
   private long maxRowsForFocus = Long.MAX_VALUE;

   /** The name of object use to get focus property in hibernate sql syntax 
    * SELECT defaultFocusPropertyObjectAlias.getFocusProperty ...	
    * 
    * @author Andrej Zachar
    */
   private String defaultFocusPropertyObjectAlias = "";

   /**
    * Enable or Disable String length checking of given filters values. If
    * filter value is null or empty is removed from query.
    * @author Andrej Zachar
    */
   private boolean _isRemoveEmptyStrings = false;

   private StatementBuilder statementBuilder;

   /**
    * Enable or disable optimalization of the query for focus property.
    */
   private boolean _focusOptimalization = true;

   /**
    * @return Returns the focusOptimalization.
    */
   public boolean isFocusOptimalization()
   {
      return _focusOptimalization;
   }

   /**
    * Enable or disable optimalization of the query for focus property.
    * @param focusOptimalization true - enable query with short select, false - query with full select
    */
   public void setFocusOptimalization(boolean focusOptimalization)
   {
      _focusOptimalization = focusOptimalization;
   }

   /**
    * <p>
    * If is set, it use special ScrollableResultsDecorator, that enable or
    * disable to add object in final list.
    * </p>
    * <h4>NOTE:</h4>
    * <p>
    * Also, it respects the total count of entries that overlap your paged
    * list.
    * </p>
    * @param validator The validator to set.
    */
   public void setValidator(ObjectValidator validator)
   {
      _validator = validator;
   }

   /**
    * @return Returns the isPrefilterEmpty.
    */
   public boolean isRemoveEmptyStrings()
   {
      return _isRemoveEmptyStrings;
   }

   /**
    * Enable or Disable String length checking of given filters values. If
    * filter value is null or empty is removed from query.
    * 
    * @param isPrefilterEmpty
    *            true-remove null and empty, false - remove only null filters.
    */
   public void setRemoveEmptyStrings(boolean isPrefilterEmpty)
   {
      _isRemoveEmptyStrings = isPrefilterEmpty;
   }

   /**
    * @see net.mlw.vlh.ValueListAdapter#getValueList(java.lang.String,
    *      net.mlw.vlh.ValueListInfo)
    */
   public ValueList getValueList(String name, ValueListInfo info)
   {

      LOGGER.debug("getValueList(String, ValueListInfo) - start");

      if (info.getSortingColumn() == null)
      {
         info.setPrimarySortColumn(getDefaultSortColumn());
         info.setPrimarySortDirection(getDefaultSortDirectionInteger());
         if (LOGGER.isDebugEnabled())
         {
            LOGGER.debug("The default sort column '" + getDefaultSortColumn() + "' with direction '" + getDefaultSortDirectionInteger()
                  + "' was  set.");
         }
      }

      int numberPerPage = info.getPagingNumberPer();

      if (numberPerPage == Integer.MAX_VALUE)
      {
         numberPerPage = getDefaultNumberPerPage();
         info.setPagingNumberPer(numberPerPage);
         if (LOGGER.isDebugEnabled())
         {
            LOGGER.debug("The paging number per page '" + numberPerPage + "' was  set.");
         }
      }

      Session session = SessionFactoryUtils.getSession(getSessionFactory(), allowCreate);
      try
      {
         Query query;

         boolean doFocus = ((getAdapterType() & DO_FOCUS) == 0) && info.isFocusEnabled() && info.isDoFocus() && (namedQuery == null);

         if (doFocus)
         {
            if (LOGGER.isDebugEnabled())
            {
               LOGGER.debug("Start to focusing adapterName '" + name + "', ValueListInfo info = " + info + "'");
            }
            ScrollableResults results = getScrollableResults(getQueryForFocus(info, session), info);
            results.beforeFirst();
            doFocusFor(info, results);

            if (LOGGER.isDebugEnabled())
            {
               LOGGER.debug("Focusing finished for adapterName '" + name + "', ValueListInfo info '" + info + "'");
            }
         }

         query = getQuery(info, session);

         boolean doPaging = ((getAdapterType() & DO_PAGE) == 0);

         List list;

         if (doPaging)
         {
            if (LOGGER.isDebugEnabled())
            {
               LOGGER.debug("getValueList(String adapterName = " + name + ", ValueListInfo info = " + info
                     + ") - Start to paging result set");
            }

            list = new ArrayList(numberPerPage);
            ScrollableResults results = getScrollableResults(query, info);

            results.last();
            int lastRowNumber = results.getRowNumber();
            info.setTotalNumberOfEntries(lastRowNumber + 1);

            if (numberPerPage == 0)
            {
               numberPerPage = getDefaultNumberPerPage();
            }

            int pageNumber = info.getPagingPage();
            boolean isResult;
            if (pageNumber > 1)
            {
               if ((pageNumber - 1) * numberPerPage > lastRowNumber)
               {
                  pageNumber = (lastRowNumber / numberPerPage) + 1;
                  info.setPagingPage(pageNumber);
               }
            }
            // fixed by liujuan 2008.6.5
            isResult = results.first();
            
            if (pageNumber > 1)
            {
            	// isResult = results.scroll((pageNumber - 1) * numberPerPage - lastRowNumber);
            	isResult = results.scroll((pageNumber - 1) * numberPerPage);
            }
            /*else
            {
               isResult = results.first();
            }*/

            for (int i = 0; i < numberPerPage && isResult; i++)
            {
               list.add(results.get(0));
               isResult = results.next();
            }

            LOGGER.debug("Sorting finished.");

         }
         else
         {

            LOGGER.debug("Retrieving a list directly from the query.");

            list = query.list();
            info.setTotalNumberOfEntries(list.size());
         }

         ValueList returnValueList = getListBackedValueList(info, list);
         if (LOGGER.isDebugEnabled())
         {
            LOGGER.debug("Retrieved list was wrapped in valuelist, info=" + info);
         }
         return returnValueList;
      }
      catch (HibernateException e)
      {
         LOGGER.error("Error getting data in adapater '" + name + "' with info = '" + info + "'", e);
         throw SessionFactoryUtils.convertHibernateAccessException(e);
      }
      catch (Exception e)
      {
         LOGGER.fatal("Fatal error getting data in adapater '" + name + "' with info = '" + info + "'", e);
         return null;
      }
      finally
      {
         SessionFactoryUtils.releaseSession(session, getSessionFactory());
      }
   }

   /**     
    * @param info
    * @param list
    * @return DefaultListBackValueList instance
    */
   protected ValueList getListBackedValueList(ValueListInfo info, List list)
   {
      return new DefaultListBackedValueList(list, info);
   }

   /**
    * @param info
    * @param results
    * @throws HibernateException
    * @throws NoSuchMethodException
    * @throws InvocationTargetException
    * @throws IllegalAccessException
    */
   private void doFocusFor(ValueListInfo info, ScrollableResults results) throws HibernateException
   {
      info.setFocusStatus(ValueListInfo.FOCUS_NOT_FOUND);

      int currentRow;
      if (isFocusOptimalization())
      {
         if (LOGGER.isDebugEnabled())
         {
            LOGGER.debug("Focusing only property '" + info.getFocusProperty() + "' == '" + info.getFocusValue() + "'.");
         }
         for (currentRow = 0; ((results.next()) && (currentRow < maxRowsForFocus)); currentRow++)
         {
            String value = results.get(0).toString();
            if (value.equalsIgnoreCase(info.getFocusValue()))
            {
               if (LOGGER.isInfoEnabled())
               {
                  LOGGER.info("Focus property '" + info.getFocusProperty() + "' in row '" + currentRow + "'.");
               }
               info.setPagingPageFromRowNumber(results.getRowNumber());
               info.setFocusedRowNumberInTable(results.getRowNumber());
               info.setFocusStatus(ValueListInfo.FOCUS_FOUND);
               break;
            }
         }
      }
      else
      {
         if (LOGGER.isDebugEnabled())
         {
            LOGGER.debug("Focusing object with the property '" + info.getFocusProperty() + "' == '" + info.getFocusValue() + "'.");
         }
         for (currentRow = 0; ((results.next()) && (currentRow < maxRowsForFocus)); currentRow++)
         {

            Object value;
            try
            {
               value = PropertyUtils.getProperty(results.get(0), info.getFocusProperty());
            }
            catch (HibernateException e)
            {
               LOGGER.error("Error getting focus property '" + info.getFocusProperty() + "'", e);
               throw e;
            }
            catch (Exception e)
            {
               LOGGER.warn("Ingoring error while getting focus property '" + info.getFocusProperty() + "'", e);
               continue;
            }

            if (value.toString().equalsIgnoreCase(info.getFocusValue()))
            {
               if (LOGGER.isInfoEnabled())
               {
                  LOGGER.info("Focus object's property '" + info.getFocusProperty() + "' was found in the row '" + currentRow + "'.");
               }
               info.setPagingPageFromRowNumber(results.getRowNumber());
               info.setFocusedRowNumberInTable(results.getRowNumber());
               info.setFocusStatus(ValueListInfo.FOCUS_FOUND);
               break;
            }
         }
      }
      if (currentRow == maxRowsForFocus)
      {
         if (LOGGER.isInfoEnabled())
         {
            LOGGER.info("Focus for property '" + info.getFocusProperty() + "' exceded maximum rows for focus '" + maxRowsForFocus + "'.");
         }
         info.setFocusStatus(ValueListInfo.FOCUS_TOO_MANY_ITEMS);
      }
   }

   /**
    * @param query
    * @param info ValueListInfo This info will be set to validator. 
    * @return ScrollableResults, if is set non null _validator, it returns the
    *         ScrollableResultsDecorator.
    * @throws HibernateException
    */
   private ScrollableResults getScrollableResults(Query query, ValueListInfo info) throws HibernateException
   {
      ScrollableResults results;

      if (_validator == null)
      {
         LOGGER.debug("Validator is null, using normal ScrollableResults");
         results = query.scroll();

      }
      else
      {
         LOGGER.info("Using decorator of the ScrollableResults with your validator.");
         _validator.setValueListInfo(info);
         results = new ScrollableResultsDecorator(query.scroll(), _validator);
      }

      return results;
   }

   /**
    * @param info
    * @param session
    * @return @throws
    *         HibernateException
    */
   private Query getQuery(ValueListInfo info, Session session) throws HibernateException, ParseException
   {

      if (getHql() != null)
      {
         return getStatementBuilder().generate(session, new StringBuffer(getHql()), info.getFilters(), _isRemoveEmptyStrings);
      }
      else
      {
         if (namedQuery != null)
         {
            return session.getNamedQuery(getNamedQuery());
         }
         else
         {
            throw new HibernateException("Please define any QUERY in value list retrieve adpater!");
         }
      }
   }

   /**
    * If focus optimalization is true, it select only focus property. For
    * validator is recommended to set it to false, while you want to validate
    * properties of retrieved objects.
    * 
    * @param info
    * @param session
    * @return
    * @throws HibernateException
    */
   private Query getQueryForFocus(ValueListInfo info, Session session) throws HibernateException, ParseException
   {
      if (isFocusOptimalization())
      {
         LOGGER.info("Focus will use optimalizated query.");
         return getOptimizedQuery(info, session);
      }
      else
      {
         LOGGER.info("Focus will use normal (full) query.");
         return getQuery(info, session);
      }
   }

   /**
    * 
    * @param info
    * @param session
    * @return query that select only focus property.
    * @throws HibernateException
    * @throws ParseException
    */
   private Query getOptimizedQuery(ValueListInfo info, Session session) throws HibernateException, ParseException
   {
      if (getHql() != null)
      {
         return getStatementBuilder().generateForFocus(session, new StringBuffer(getHql()), info.getFilters(), _isRemoveEmptyStrings,
               defaultFocusPropertyObjectAlias, info.getFocusProperty());

      }
      else
      {
         throw new HibernateException(
               "Please define any HQL QUERY in value list retrieve adpater, function is not implemented for NamedQuery!");
      }
   }

   /** Set the Hibernate SessionFactory to be used by this DAO.
    * 
    * @param sessionFactory The Hibernate SessionFactory to be used by this DAO.
    */
   public final void setSessionFactory(SessionFactory sessionFactory)
   {
      this.sessionFactory = sessionFactory;
   }

   /** Return the Hibernate SessionFactory used by this DAO.
    * 
    * @return The Hibernate SessionFactory used by this DAO.
    */
   protected final SessionFactory getSessionFactory()
   {
      return this.sessionFactory;
   }

   /** Sets the hql used to retrieve the data.
    * @param hql The hql to set.
    * @deprecated use setHql(String)
    */
   public void setHsql(String hql)
   {
      this.hql = hql;
   }

   /** Sets the hql used to retrieve the data.
    * @param hql The hql to set.
    */
   public void setHql(String hql)
   {
      this.hql = hql;
   }

   /** Returns the namedQuery.
    * @return Returns the namedQuery.
    */
   public String getNamedQuery()
   {
      return namedQuery;
   }

   /** Sets the namedQuery.  
    * <p>
    *   NOTE: by using this you can not enable sorting, filtering,
    *   paging of the data, and  focusing of rows.
    * </p>
    * @param namedQuery
    *            The namedQuery to set.
    */
   public void setNamedQuery(String namedQuery)
   {
      this.namedQuery = namedQuery;
   }

   /** Gets the hql used to retrieve the data.
    * @return Returns the hql.
    */
   public String getHql()
   {
      return hql;
   }

   /** Sets: If a new Session should be created if no thread-bound found.
    * 
    * @param allowCreate
    *            The allowCreate to set.
    */
   public void setAllowCreate(boolean allowCreate)
   {
      this.allowCreate = allowCreate;
   }

   /**
    * Maximum rows to search with Focus
    * 
    * @return Returns the maxRowsForFocus.
    */
   public long getMaxRowsForFocus()
   {
      return maxRowsForFocus;
   }

   /**
    * Maximum rows to search with Focus
    * 
    * @param maxRowsForFocus
    *            The maxRowsForFocus to set.
    */
   public void setMaxRowsForFocus(long maxRowsForFocus)
   {
      this.maxRowsForFocus = maxRowsForFocus;
   }

   /**
    * @return Returns the defaultFocusPropertyObject.
    */
   public String getDefaultFocusPropertyObjectAlias()
   {
      return defaultFocusPropertyObjectAlias;
   }

   /** 
    * The name of object use to get focus property in hibernate hql syntax 
    * SELECT defaultFocusPropertyObjectAlias.getFocusProperty ...  
    * 
    * @param defaultFocusPropertyObjectAlias
    *            The defaultFocusPropertyObjectAlias to set.
    */
   public void setDefaultFocusPropertyObjectAlias(String defaultFocusPropertyObjectAlias)
   {
      this.defaultFocusPropertyObjectAlias = defaultFocusPropertyObjectAlias + ".";
   }

   /**
    * @return Returns the statementBuilder.
    */
   public StatementBuilder getStatementBuilder()
   {
      if (statementBuilder == null)
      {
         statementBuilder = new StatementBuilder();
      }
      return statementBuilder;
   }

   /**
    * @param statementBuilder The statementBuilder to set.
    */
   public void setStatementBuilder(StatementBuilder statementBuilder)
   {
      this.statementBuilder = statementBuilder;
   }
}